Repository navigation
[OMNIML-6071] Calibrate a distributed model on per-rank data - #2740
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
1b34e71 to
c99d63a
Compare
7f91f94 to
963e78a
Compare
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2740 +/- ##
==========================================
+ Coverage 78.47% 81.89% +3.41%
==========================================
Files 676 664 -12
Lines 77509 74849 -2660
==========================================
+ Hits 60827 61298 +471
+ Misses 16682 13551 -3131
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/ok to test 963e78a |
c99d63a to
d0d9b5e
Compare
963e78a to
935bd7f
Compare
|
/ok to test 935bd7f |
935bd7f to
c3ccb86
Compare
d0d9b5e to
64f8588
Compare
c3ccb86 to
96159d7
Compare
64f8588 to
7da05a2
Compare
|
/ok to test 96159d7 |
96159d7 to
540716e
Compare
7da05a2 to
00e5f72
Compare
|
/ok to test 540716e |
00e5f72 to
42a2de6
Compare
540716e to
177c692
Compare
…y weight gather The token count now rides in the fp32 losses' buffer, exact for any calibration set, so each module issues one all-reduce instead of two. And every module's reduction runs in its own pass ahead of the postprocess pass, whose weight access an FSDP2 model gathers for, instead of alternating with those gathers. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The data-parallel sync stacked every candidate clip's loss into a new tensor, packed the token count after it and divided out of place, so while it ran it held up to three copies of the losses beyond the module's own. Each candidate's loss is now a view of one buffer, which the sync reduces and averages in place, and the token count goes in its own int64 all-reduce, exact for any count. Every module's reductions still run ahead of the weight gathers. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal advised calibrating with max, MSE, GPTQ, SmoothQuant or awq_clip "with distributed_sync on", but only max and MSE have that option. And local_hessian, refused although it has the option, says why where it is listed: its distributed_sync syncs only the max amax it starts from. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…uantizing The calibration guard runs before quantization converts anything, so on an expert-parallel plan it reads the routed experts' ParallelState from the caller, as the expert-parallel load seeds it, and checks their quantized modules again before they calibrate. The docstring now says so instead of implying that only the converted modules are checked. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_StridedIterableShard takes one sample in world_size from the stream each rank builds, so every rank still reads and decodes all of them. Its docstring now says so, and that a map-style dataset, or a source that shards itself, saves that work for a stream costly to decode. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Main now refuses AWQ-Lite and AWQ-Full on an FSDP2 model sharded over several ranks, and its README note on that sits right above this PR's note on nvfp4_awq_clip. The two notes are now separate paragraphs, and this PR's says why nvfp4_awq_clip is not refused: AWQ-Clip folds no smoothing scale into the weights, and its clip search is averaged over the ranks. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
AutoQuantize calibrates and scores through forward_step on the model it has converted, and conversion drops the expert-parallel forwards that distributed_forward_loop restores. On an expert-parallel model the first of those forwards would fail with the experts forward's missing process_group error, after the model was converted. auto_quantize now refuses a model whose recorded plan shards its experts, before it changes anything, as quantize and calibrate refuse what they cannot calibrate. A two-rank test with seeded experts and an INT8 candidate checks the module types and state are left as they were. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
shard_dataloader replaces a loader's sampler with the rank's DistributedSampler over the loader's dataset. It checked a RandomSampler's sample count against the sampler's own source, so a SequentialSampler, RandomSampler or DistributedSampler over a shorter source than the dataset passed, and the shard then calibrated on samples the loader never read. The coverage is now compared with the loader's dataset; a sampler that orders or shuffles the whole dataset is still replaced as before. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Only the three known samplers reach len(source), and each has a data_source or a dataset; the empty default only keeps the expression typed. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_reinstall_a2a_forwards planted the torch_a2a wrapper on any planned module whose instance forward was missing or a bound method. The clobber it repairs always leaves a bound method: a module with no instance forward is one the load never wrapped, and planting there would add the style's transforms to a module that ran without them. It now plants only over a bound method, as its docstring says. The pre-hook test now starts from rebound forwards, and a new test checks that a module the load did not wrap is left alone. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal matches calibrate modes by name, so a rank-local algorithm renamed or misspelled in _RANK_LOCAL_CALIBRATION would silently stop being refused. A unit test now checks each name against CalibrateModeRegistry, and the comment says a new rank-local algorithm has to be added to the list by hand. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel test added a module-level import of Qwen3MoeConfig, which made the grouped_mm bit-for-bit job's own import of it redundant. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The docstrings of _StridedIterableShard, _reinstall_a2a_forwards and _refuse_unsupported_distributed_calibration, and the pre-hook comment in _reinstall_a2a_forwards, keep what each says about why and drop what the code already shows. No code changes. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 5829a85 |
There was a problem hiding this comment.
Pensieve lgtm review using chenhany's profile (revision 12).
LGTM: prior correctness concerns are resolved and the size concern no longer applies; the large test addition and justified import refactor need human sign-off.
Needs action:
- Confirm human sign-off on the distributed calibration tests and
tests/gpu/torch/utils/test_moe_a2a.pyimport refactor; the pre-existing transport case and assertions are retained.
No action needed:
- ✔️ Earlier fixes remain present, including EP AutoQuantize's pre-conversion refusal, sampler coverage checks, EP/EDP validation, falsy-sync refusal, copy-free AWQ synchronization, and real EP/FSDP2 lifecycle coverage.
- The split request in
5725165745no longer applies: the four production-file diffs total 489 added lines before excluding comments and blanks, already below the guideline. No size exception is needed. - The sampler-identity objection (
4433247475) is answered: these built-in samplers yield indices applied to the loader's dataset, so equal-length sources do not widen coverage. - The new test header matches
LICENSE_HEADER. No new material defects found. - Baseline:
2d4e8d019e72bcc7dc516a788f8e406e543e9e17. Tests were not executed; CI results were unavailable. Duplicate checking covers supplied history only; some older review bodies are empty.
Review baseline: https://github.com/NVIDIA/Model-Optimizer/blob/2d4e8d019e72bcc7dc516a788f8e406e543e9e17/.agents/review-guidelines/code-review.md
Review run: 1a478e958042429897fe21950abb575f
|
Sign-off on the test changes Signing off on this PR's tests:
|
kevalmorabia97
left a comment
There was a problem hiding this comment.
Every rank issues the same collectives in the same order, including for MoE experts that see different forward counts, and each guard has a test that fails without it.
…veral ranks The guard against calibration that keeps its statistics on the rank matched algorithm names against a list kept by hand in model_quant.py, so a newly registered algorithm passed it without anyone deciding that it syncs across ranks. Each calibrate mode descriptor now declares _distributed_calibration, beside _supports_layerwise and its calib function: True for none, max, mse, smoothquant, awq_clip, gptq, nvfp4_act_headroom and lsq; False for awq_lite, awq_full, svdquant, local_hessian and imatrix. A mode that declares neither is refused on a model distributed over several ranks. The descriptor also checks its own options and nested algorithms: distributed_sync turned off, and the algorithms a config runs for its scales, as before. The 13 built-in modes refuse exactly what they refused before; _RANK_LOCAL_CALIBRATION is gone. The registry-name test becomes a check that every calibrate mode modelopt defines declares, and a new test registers a mode that does not and checks that calibrating with it is refused under a multi-rank plan. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Removing _rank_local_calibrations left one blank line between __all__ and _group_ranks. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 4991150 |
|
/claude review |
There was a problem hiding this comment.
Approve: the calibration guards, per-rank sharding and awq_clip's once-per-module reduction read correct at this head, and no new defect surfaced.
No action needed:
- ✔️ Resolved since the last rounds: the
auto_quantizeexpert-parallel refusal, the sampler-source check, the bound-method-gated forward re-plant, and registry coverage —_RANK_LOCAL_CALIBRATIONis now a_distributed_calibrationflag on each mode descriptor defaulting to fail-closedNone, extending the existing mode mechanism rather than adding a parallel one. - Re-derived from source:
dict(zip(clip_ratios, loss_buffer))hands out views, so one in-place all-reduce covers every candidate; a tokenless rank adds zeros to all candidates equally, leaving the argmin intact;num_tokensis reduced too, so a DP group agrees on thepostprocessgather;_names_the_experts_groupsaccepts only the ep-innermost(edp, ep)layout;_max_batch_size's arg tuple matchesget_max_batch_sizeand the planned branch runs exactly one forward. - Under the 500 core-logic budget once
distributed.py's large docstrings are subtracted; new test header matchesLICENSE_HEADER. tests/gpu/torch/utils/test_moe_a2a.py:41still importsDistributedConfigfromtransformers.distributedat module scope while the file guards on 5.6/5.16 — worth a glance, but I could not substantiate the earlier claim that it breaks collection withintransformers>=5.5,<5.19.- Limits: baseline
2d4e8d019e7, head49911504. No tests run, no CI result; the review-comments API was unavailable here, so prior inline threads were read from review summaries only.
…e code get_max_batch_size's docstring now says who must call it on a model distributed over several ranks, what size it returns there and that it is not retried on out-of-memory; why not, the collectives a retry or an out-of-memory forward would leave waiting, moves to a comment in the planned branch. shard_dataloader's docstring keeps the inputs it takes, how it samples and what the caller must provide; why a stream loaded with workers is refused, why every rank's stream must match and why the split is padded move to comments beside those checks. No code changes. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test e8d80fe |
…document the flag Nothing checked that a mode declaring _distributed_calibration = True calibrates under a multi-rank plan: flipping GPTQ's declaration to False passed every test. A test now runs the guard, under a two-rank plan, for each of the eight modes expected to declare True; it fails with GPTQ's flipped. The custom calibration section of the quantization guide now says that a custom mode declares the flag, what True and False mean, and that a mode declaring neither is refused on a model distributed over several ranks; its example sets it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 75012d5 |
Note
The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [8/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.
Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.
Series layout
mainWhat does this PR do?
Type of change: New feature
The pieces a calibration loop needs once the model is spread across ranks.
shard_dataloadergives each rank its own shard of the data.DistributedSamplerwithdrop_lastoff, so every rank runs the same number of steps; a rank that exits early hangs the others on the per-step collectives.IterableDatasetgets a strided copy of the stream, which must yield the same samples in the same order on every rank (a stream that shuffles needs a fixed seed of its own). A streaming loader with worker processes is refused: a dataset that splits itself across workers, as a Hugging Face streaming dataset does, would lose samples silently. It is not main'svlm_dataset_utils._ShardedIterable, which pads each rank to a count its loader knows: this wraps any loader's stream, which may have no length.distributed_forward_loopbuilds theforward_loopthatmtq.quantizecalls. It moves each batch to thedeviceit is given and runs the full distributed forward, so the expert-parallel collectives fire inside it, with KV caching off as on the single-process path. Each batch is a mapping of the forward's keyword arguments; anything else is refused with aTypeErrorat the first batch.ep_meshits experts are sharded over, and the loop re-plants thetorch_a2aforward wrappers that quantizer insertion clobbers on transformers >= 5.16. A module whose forward is still the wrapper planted there is skipped, so passes do not stack wrappers; one whose forward was rebound since (another conversion, an export) is planted again. The planted wrappers stay on the model, so its later forwards (a preview's, an export's) work once the loop has run.mtq.auto_quantizecalls itsforward_stepdirectly and would plant nothing, so it refuses a model whose recorded plan shards its experts, before converting anything. Any other model passes none. A mesh missing for an expert-parallel model, or given for another, raises.--batch_size 0sizes calibration withget_max_batch_sizeon each rank: it measured every visible GPU, and on an out-of-memory error halved the batch and probed again, a different number of collective-bearing forwards per rank. For a model whose recorded plan spreads it over several ranks it is now collective: each rank measures the GPU it computes on (torch.cuda.current_device()) from one forward of the single-sample input, without probing the size it arrives at (a rank running out of memory partway through a probe's forward would leave its peers waiting in that forward's collectives), and every rank takes the smallest size any rank allows, reduced withall_reduce(..., min)([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736). A single process, and a model with no plan recorded (a Megatron model, say), are unchanged.maxall-reduces its amax, GPTQ merges its Hessians across data-parallel replicas, and MSE and SmoothQuant work from max's synced amax. AWQ-Lite does not: it averages its activation scales over data parallelism but picks each layer's scale from its own rank's search, and a 2-rank probe showed the replicas' weights andpre_quant_scalediverging.awq_clipnow averages each candidate clip's loss over data parallelism before the ranks pick one, in both branches: each rank accumulates its own losses, and after the forward loop every module's losses and token count are all-reduced once, in module order, so the collective count does not depend on how many forwards a rank ran a module (an MoE expert's is each rank's own). Its static-block branch averaged per forward before, and its per-tensor branch, which NVFP4 takes, not at all; this also makes Megatron data-parallel replicas agree on the per-tensor clip.local_hessianandimatrixshare nothing either, and neither do max and MSE withdistributed_syncoff.mtq.quantize,mtq.calibrateandmtq.auto_quantizerefuse these (awq_lite,awq_full,svdquant, which runsawq_lite,local_hessian,imatrix, and any config that turnsdistributed_syncoff, withFalseorNone, also as another algorithm'sweight_scale_algorithmorscale_algorithm) on a model whose recorded plan spans several ranks, before converting anything (auto_quantizein its format check). A model with no plan recorded, such as a Megatron model, is unchanged. On an expert-parallel plan, any calibration is refused unless every module holding routed experts carries, as its expert-parallel group, the rank's row of the(edp, ep)mesh the expert-parallel load lays the ranks out in, ep innermost (in row order, which picks each rank's expert shard), and, as its data-parallel group, its column, the plan'sedpranks holding the same experts: each rank's per-expert quantizers hold its own experts, and max's and GPTQ's data-parallel sync would otherwise merge different experts' scales. The modules holding routed experts are those carrying an expert-parallel group and, once quantization has converted them, the quantized modules under a container namedexperts, as transformers' expert-parallel plans name it; unseeded experts beside a seeded router are refused once converted, before they calibrate. [OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741's expert-parallel loader gives the routed experts those groups. A module with no routed experts and no expert-parallel group, such as a VLM's vision tower that hf_ptq quantizes on its own with every quantizer disabled, passes.--use_fsdp2path is not changed here. Its load (parallel_load_and_prepare_fsdp2) records no plan, so the batch sizing and the refusal above do not apply to it yet. Recording the plan there lands with [OMNIML-6083] Write a sharded model's HF checkpoint without gathering it, and export an expert-parallel model through it #2752, the first PR whose export reads it on that path, together with stopping a planned run from retrying a batch after a rank-local out-of-memory error; [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754 then moves the load ontoload_pretrained, which records the plan itself.nvfp4_awq_clip's data-parallel clip (below) needs no plan, so it applies to--use_fsdp2runs now.shard_dataloaderrefuses a loader that picks its own samples (a custombatch_sampler, a sampler other than sequential, random or distributed, or one of those over another source than the loader's dataset, also inside a plainBatchSampleror with auto-batching off) instead of rebuilding it, and keeps a plainBatchSampler's batch size anddrop_lastin the rank's shard; a loader with auto-batching off (batch_size=None) shards one item per step, as it loads. With one rank,shard_dataloaderreturns the loader itself, its sampler and shuffling included.distributed_forward_loopruns its forwards without gradients.get_max_batch_sizereads the recorded plan, as the calibration guard does.distributed_forward_looprefuses anep_meshwhose size is not the plan'sep_size.Usage
Testing
New
tests/unit/torch/utils/test_distributed_calibration.py(42 cases), on CPU, in one process except where noted. It covers:BatchSampler's batch size anddrop_lastincluded;use_cacheoff and the allocator cache freed around it in a process that started CUDA, and not in one that did not; a batch that is not a mapping refused;ep_meshrefused when missing for an expert-parallel plan, given for another, or over the whole world for anep_size=2plan;quantize,calibrateandauto_quantize(before conversion), also when another algorithm runs it for its weight scales or max is told not to sync, with the model left as it was;distributed_forward_loopruns on real GPU ranks in [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742's expert-parallel calibration test, which checks each rank's per-expert scales against the whole model; this PR's own tests run it in one process.tests/unit/torch/utils/test_dataset_utils.py, on two real gloo ranks (cpu_dist_workers) with only the CUDA memory queries faked per rank: each rank measures only its own GPU, probes once, and both take the smaller batch size. A rank keeping its own size, measuring every GPU, or skipping the reduction fails it, the last after the group's 10 s timeout rather than by hanging; main's single-process retry test still passes. The two-rank test also runs a model with no plan recorded, which each rank measures on every visible GPU, as a single process does, without a refusal.Mutation-checked: deciding the reduction or the mesh other than by the plan fails a test (5 mutants), and so does dropping either refusal call, the nested-algorithm check, the
distributed_synccheck, hf_ptq's plan recording, the expert-parallel group refusal, or theep_meshsize check. Refusing every expert-parallel calibration, or reading the plan strictly inget_max_batch_size, fails a test too. Dropping the check on converted experts or the data-parallel size check, keeping a replant flag once set, or planting on every pass each fail a test. Rebuilding a lone rank's loader, refusing a module without routed experts, or passing any module with no expert-parallel group each fail a test too.The shard tests are one table over dataset kind and world size, the two replant tests are one, and cases a remaining test covers are dropped. Each drop is checked by mutation.
A subset sampler is refused, also under a plain
BatchSamplerand with auto-batching off, and so is a custom batch sampler; a lone rank keeps each such loader as it is. The sampler, batching, non-mapping-batch andauto_quantizecases each fail without their fix (mutation-checked).Locally on this revision, the stack rebased on main c6fa613: pre-commit over the whole PR against main, at every PR of the stack; and at the stack's tip the full
tests/uniton transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.GPU tests were not re-run on this revision.
Re-cut on 2026-10-27 (see the note at the top): pre-commit, mypy included, passes at this head over the whole change against main, and the series' PRs merged together give a tree byte-identical to the former stack's tip. This PR's own unit tests pass at this head on CPU (torch 2.14, transformers 5.18); CI runs the full unit suite.
Review fixes, 2026-10-27: the a2a forward wrappers are re-planted only where quantizer insertion rebound the forward, so a wrapper still in place is never wrapped twice (found by review: the double wrap ran a module's input transforms twice, which no real run has shown to fail; on 4 GPUs at transformers 5.16.1 the expert-parallel load tests pass with either behaviour, and the CPU calibration-loop test is what tells them apart); the install probe checks each style it calls. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
Review fixes, 2026-10-27, round 2: the batch size is measured on the GPU the rank computes on (
torch.cuda.current_device()); a streaming loader with workers is refused; the docstring and the hf_ptq README say why the measurement does not probe. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.Rebased on 2026-10-27 onto main 7c3c868, after [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739 merged: pre-commit, mypy included, and this PR's own unit tests pass at this head, and the series' PRs merged together add no unit-test failure over main (tests/unit/torch utils, export, quantization and models, on transformers 5.18 and 5.5.4 and on torch 2.8). The hf_ptq README's FSDP2 AutoQuantize note follows main's KV-cache AutoQuantize support ([OMNIML-5987] [2/3] Support DP/FSDP2 KV AutoQuant and checkpoint resume #2664): weight and KV-cache AutoQuantize with
--use_fsdp2are experimental, and refused when a candidate format is one of those this PR refuses.Review fix, 2026-10-27 night:
shard_dataloaderrefuses aRandomSamplerthat does not draw the whole dataset once (othernum_samples, or with replacement), as it refuses other samplers.Review suggestion, 2026-10-27, last round: the comment over the sync guard names the algorithms' defaults it relies on.
Rebased on 2026-10-28, late night, onto [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736 at
cf4597dcbeb, on main1ca8bd1fc4d(after [OMNIML-6073] [1/3] Gather only FSDP2's own parameters in its weight-access window #3088 and [OMNIML-6073] [2/3] Quantize fused experts under grouped_mm; leave transposed layouts unquantized #2745 merged), without conflicts; this PR's own diff is unchanged. Its tests with [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736's and thehf_ptqargument tests pass on transformers 5.18 (220), and pre-commit passes against main.Rebased on 2026-10-28, late night, onto main
bdc45368d10after [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736 merged; [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736's squash has the tree of its last head, so this PR's tree is unchanged.Review fixes, 2026-10-29: the two-rank batch-size test makes its bounded group with
bounded_collectives, whose barrier keeps a rank from destroying the group while its peer is still connecting (one CI run had failed so); the batch sizing's distributed helpers are imported at module top; the rank-local refusal reads each algorithm's options through its mode's schema, so adistributed_syncof"false"is refused; the experts' groups are checked by the ranks they span, an expert-parallel group ofep_sizeranks and a data-parallel group ofedpranks meeting it only at the rank itself (a test answers four ranks' group queries, which the two-rank pool cannot build). Each behaviour fix fails its test without it (mutation-checked). Pre-commit passes against main; this PR's tests,test_distributed.py,test_quantize_cpu.pyand the hf_ptq argument tests pass on CPU with transformers 5.18 and 5.5.4.Review fix, 2026-10-29, second round:
awq_clip's per-tensor branch averages each candidate clip's loss over data parallelism, through the helper its static-block branch now uses too (same group, same op, every rank in the same order), andawq_clipis allowed on a distributed model; this also makes Megatron data-parallel replicas agree on the per-tensor clip. A two-rank CPU test of the static-block branch fails without the averaging (mutation-checked); a two-GPU NVFP4 test of the per-tensor branch is intests/gpu(NVFP4 fake quantization needs CUDA; not run here). The hf_ptq README and the changelog saynvfp4_awq_clipworks with--use_fsdp2on several GPUs. Pre-commit passes against main;test_calib.pyand this PR's tests pass on CPU.GPU check, 2026-10-29, of the NVFP4 per-tensor branch on GB200 (torch 2.11.0a0, transformers 5.9.0), with the second round's per-forward averaging; without the data-parallel mean the even and odd ranks pick different per-tensor clips:
test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_rankGPU check, 2026-10-29, at
cdb4e952468, with the fourth round's once-per-module averaging after the forward loop, on GB200 (torch 2.11.0a0, transformers 5.9.0); with that averaging disabled, the even and odd ranks pick different per-tensor clips again:cdb4e952468test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_ranksync_across_dpdisabled (returnas its first statement)GPU check, 2026-10-29, at
01a1df717e3, with the fifth round's empty-input guard, on GB200 (torch 2.11.0a0, transformers 5.9.0). The test now also runs a second linear on no tokens on the odd ranks; without the guard, that linear's clip stays 0 on every replica:01a1df717e3test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_rank_clip_searchamax[1] > 0: amax = [100, 0]Review fixes, 2026-10-29, third round: a
distributed_syncofNone, which skips the sync asFalsedoes, is refused too, nested scale algorithms included; the experts' groups are checked against the(edp, ep)mesh, the expert-parallel group as the rank's row in row order and the data-parallel group as its column, so diagonal data-parallel groups and a reordered expert-parallel group are refused (the four-rank test adds both). Each fails its test without the change (mutation-checked). Pre-commit passes against main; this PR's tests pass on CPU.Review fixes, 2026-10-29, fourth round:
awq_clipaverages its clip search over the data-parallel ranks once per module after the forward loop, with the token count, in both branches, instead of once per forward, which hung when ranks ran an MoE expert a different number of times (the two-rank test now does; it fails without the change, on its bounded group's timeout) (the same round accepted the experts' groups of a mesh with either axis innermost; the eighth round reverts that). Pre-commit passes against main;test_calib.pyand this PR's tests pass on CPU, and the quantization unit tests pass with the change.Review fix, 2026-10-29, fifth round:
awq_clipskips its clip search for an input with no tokens, which an MoE expert a rank routes none to can still get. Before, the per-tensor branch added NaN losses, which the post-loop sum spread to the replicas that had tokens (a clip of 0 for them), and the static-block branch failed to reshape the empty input. The two distributed tests now run an expert on no tokens on one rank, and check that its clips are positive and those a single process picks from the other ranks' tokens; each fails without the change (mutation-checked; the GPU one in the table above). Pre-commit passes against main;test_calib.pyand this PR's tests pass on CPU.Review fix, 2026-10-29, sixth round: hf_ptq's
--use_fsdp2plan recording, its README notes, changelog entry and argument test leave this PR for [OMNIML-6083] Write a sharded model's HF checkpoint without gathering it, and export an expert-parallel model through it #2752 (see above); the new GPU testtests/gpu/torch/quantization/test_fsdp2.py::test_planned_batch_size_then_max_calibration(2+ GPUs) sizes an FSDP2 model's batch by its recorded plan with the realget_max_batch_size, rank 0's own estimate far above the others', checks every rank takes the smallest, then calibrates each rank's shard with max throughdistributed_forward_loopand checks the steps and scales agree. Its logic ran on two CPU ranks (FSDP2 on gloo, the CUDA memory queries faked): it passes, and without the batch size's reduction it fails at once on the sizes ([40, 4]); GPU results in the table below. Rebased onto main2b900c8fa41; this PR's own diff is otherwise unchanged. Pre-commit passes against main.GPU check, 2026-10-29, at
10b3c1f0e35, of the new FSDP2 test on GB200 (torch 2.11.0a0, CUDA 13.1, NCCL 2.29.2, transformers 5.9.0); without the cross-rank minimum, rank 0 keeps its own estimate while the others take theirs:10b3c1f0e35test_fsdp2.py::test_planned_batch_size_then_max_calibrationall_reducein the planned branchReview fix, 2026-10-29, seventh round: stacked on [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 (rebased from main
2b900c8fa41onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074'sec9e09c3fc0; this PR's own diff is unchanged). New GPU testtests/gpu/torch/utils/test_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_model(2+ GPUs, transformers ≥ 5.16): a tiny Qwen3-MoE loaded by transformers' own expert-parallel load on thetorch_a2astyles and experts forward, quantized with FP8 throughdistributed_forward_loop, each rank on its own batch; every expert's weight scales and every activation scale match the whole model calibrated on every rank's batch, with bounded collectives. Its logic ran on two CPU ranks with transformers 5.18: it passes, re-planting the forward wrappers quantizer insertion drops, and fails without the re-plant, or with the conversion forcing eager as before [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074. GPU results in the table below. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 and main.GPU check, 2026-10-29, at
d90a479b6b5, of the new expert-parallel test on GB200 (torch 2.11.0a0, CUDA 13.1, NCCL 2.29.2, transformers 5.18.0): the expert-parallel calibration matches the whole model's, and needs both the forward re-plant and [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074:d90a479b6b5test_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_model_reinstall_a2a_forwardsreturns 0)process_group"normalize_experts_impl_on_the_flyforcing eager, main's behaviour)Restacked on 2026-10-29 onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 at
f68837967d3, now [2/2] on [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122 [1/2] after [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's split; no conflicts, and this PR's own diff is unchanged. The new expert-parallel test's logic still passes on two CPU ranks on this base. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's head.GPU check, 2026-10-29, at
54f0c1fd8e9(on [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074f68837967d3, on [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #312233736db61fa), on GB200 (torch 2.11.0a0, transformers 5.18.0): the expert-parallel calibration still matches the whole model with [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122's per-rank expert calibration underneath:54f0c1fd8e9test_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_modelReview fixes, 2026-10-29, eighth round (Claude's review of
54f0c1fd8e9): the experts' groups are checked against the ep-innermost(edp, ep)mesh only, the layout the expert-parallel load builds: accepting either axis innermost let transposed groups pass whenedp == ep_size(a new four-rank case,groups-transposed, fails with that check).awq_clipreduces each module's clip search in one all-reduce, the token count riding in the losses' buffer (the ninth round reduces the losses in place instead), and every module's reduction runs before any weight gather. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074;test_calib.pyand this PR's tests pass on CPU, and dropping the token count's sync fails the two-rank test.On GPUs, with the rest of the expert-parallel series on top (transformers 5.13), before this round's change to
awq_clip:Qwen3-30B-A3B at EP 4 and EP 2 on 4 GPUs, deterministic, batch size 1: every exported scale is bit-identical between the two. In the EP 2 run (edp 2), the same row/column check, reading the groups from the field that stack's loader seeds, accepted the loader's real
(edp, ep)groups.weight_scaleinput_scalek_scalev_scaleQwen3-0.6B with
nvfp4_awq_clipand--use_fsdp2on 2 GPUs:awq_clipran and the sharded checkpoint was exported, end to end. That run had the per-forward averaging this round replaces.Review fixes, 2026-10-29, ninth round (Claude's review of
2dfeb9c23f0):awq_clip's candidate losses are views of one buffer, which the sync all-reduces and averages in place, with the token count in its own int64 all-reduce: no copy of the losses, where the packed buffer held up to three, and every module's reductions still run before any weight gather. The refusal namesdistributed_synconly for max and MSE, the calibrations that have it. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074;test_calib.pyand this PR's tests pass on CPU, and the two-rank test fails without either all-reduce, or with the candidates' losses copied out of the buffer.Review follow-up, 2026-10-29, tenth round (Claude's review of
b517c7581b6): a docstring fix (07affa0fe76). The new expert-parallel test's job, run on two CPU ranks with transformers 5.18, shows that its dense linears areQuantLinearwith the seeded data-parallel group and no tensor-parallel group (Qwen3-MoE's expert plan shards no dense module). Seeding them without that group fails the test:o_proj's input amax is off by 0.293 (relative).Review follow-up, 2026-10-29, eleventh round (Claude's review of
07affa0fe76): a docstring note (e546bcca0b1) that every rank still reads and decodes every sample of a sharded stream. The experts' check thatquantize()runs before conversion reads the groups a loader seeds. [OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741's load is the first to seed them; until it lands, the unit tests and the expert-parallel GPU test, which seed them by hand, exercise that check.Rebased on 2026-10-30 onto main
99c3834f62cthrough [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #31221e74c31b49aand [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #30743f3544435d8(from [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074'sf5d36d4f794), asfd036d44f28; no conflicts, and this PR's own diff is unchanged. Pre-commit passes against main.GPU check, 2026-10-30, at
fd036d44f28, on GB200 (torch 2.11.0a0, transformers 5.9.0):awq_clip's in-place data-parallel sync (the ninth round's change) still gives every rank the same clip:fd036d44f28test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_rankRestacked on 2026-10-30 after [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122 merged: onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074
39697ad985b, which is on main55898ee4e3b(from3f3544435d8), as3f56acfbb76; no conflicts, and this PR's own diff is unchanged. Pre-commit passes against main. This PR's unit tests (test_distributed_calibration.py,test_calib.py,test_dataset_utils.py,test_distributed.py) pass on CPU with transformers 5.18 and 5.5.4, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Rebased on 2026-10-30 onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074
a6758d77eb3, on main059588b89a5, which has [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128 (from3f56acfbb76), as186efae02e5. [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's AWQ guard (_refuse_awq_on_a_sharded_fsdp2_model) and this PR's (_refuse_unsupported_distributed_calibration) met at the same three call sites inmodel_quant.py:calibrate(),quantize()andauto_quantize's candidate check. Both stay, [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's first. The changelog and the README keep both sides, and186efae02e5makes this PR'snvfp4_awq_clipREADME note its own paragraph, saying why it is not refused. This PR's own diff is otherwise unchanged. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.py(with [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's guard test) and this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Rebased on 2026-10-30 onto main
d464d30b120after [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 merged ([OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's squash; from186efae02e5), as9a87d153c89: [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's own commits drop out; no conflicts, and this PR's own diff is unchanged, with [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's guard still first at the three call sites. This PR now targets main, so the full CI runs, GPU included. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.pyand this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Review fixes, 2026-10-30, twelfth round (cjluo-nv's review of
9a87d153c89):mtq.auto_quantizerefuses a model whose recorded plan shards its experts, before converting it (fbbe2275cf8); a two-rank test with seeded experts and an INT8 candidate checks the module types and state are unchanged, and fails without the refusal.shard_dataloadercompares a sampler's source with the loader's dataset (e1976572627, typed in9ac1486cdab); a SequentialSampler, RandomSampler or DistributedSampler over a shorter source is refused, and the three new cases fail with the old check. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.pyand this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Review fix, 2026-10-31, thirteenth round (Claude's review of
9ac1486cdab): the forward re-plant now plants only over a bound method, the forward quantizer insertion leaves, as its docstring says (aba16882660); a planned module whose forward the load never wrapped is left alone, which a new test checks and the old guard fails. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.pyand this PR's unit tests pass, and the expert-parallel GPU test's job still re-plants the two experts' wrappers and passes on two CPU ranks with transformers 5.18.Rebased on 2026-10-31 onto main
e352caca3c3(fromaba16882660, adding Run fused-MoE expert GPTQ updates on all of this process's GPUs #3002, Collect imatrix statistics with one fused float32 reduction #3044 and [OMNIML-6273] Let the Claude review bot submit its review again #3156), asdea9b793553; no conflicts (Collect imatrix statistics with one fused float32 reduction #3044 and Run fused-MoE expert GPTQ updates on all of this process's GPUs #3002 touchmodel_calib.py, which merges cleanly), and this PR's own diff is unchanged. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.py, the ggml GPTQ and imatrix unit tests and this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Review follow-up, 2026-10-31, fourteenth round (Claude's review of
dea9b793553): a unit test checks that every calibration_RANK_LOCAL_CALIBRATIONrefuses names a registered calibrate mode, so a renamed or misspelled one fails instead of silently losing its refusal (d10be569b04; it fails with one name misspelled);7791f6d0f3bdrops a redundant test import. The sampler check keeps comparing lengths (see its thread). CI's GPU job passed atdea9b793553; the three multi-GPU tests skip on its single-GPU runner, and their run at7791f6d0f3bon 2 and 4 GPUs is below. Pre-commit passes against main; the changed test files pass on CPU with transformers 5.18 and 5.5.4.GPU check, 2026-10-31, at
7791f6d0f3b, on 2 and 4 GPUs (torch 2.15, transformers 5.18.0): all three multi-GPU tests pass, none skipped. Call times; the first test also pays the shared worker pool's startup, so times do not compare across tests:7791f6d0f3btest_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_ranktest_fsdp2.py::test_planned_batch_size_then_max_calibrationtest_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_modelTrimmed on 2026-10-31 (
95148c3ff8b), after a review counted the PR over the ~500-line budget: four docstrings and a comment that restated the code are shorter, with no code change. 492 added source lines (git diff --shortstat e352caca3c3...95148c3ff8b -- . ':!tests' ':!docs'). Pre-commit passes against main, and the unit tests pass on CPU with transformers 5.18 and 5.5.4.Rebased on 2026-11-01 onto main
cf38ec30ca4(from95148c3ff8b), asff372fbc30c: no conflicts, range-diff 63 of 63 commits unchanged; pre-commit with all hooks, mypy included, passes against main; on CPU, 272 unit tests pass with transformers 5.18 (the expert-parallel job on two CPU ranks included) and 271 with 5.5.4.Rebased on 2026-11-01 onto main
51645db66dcafter [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140 merged (fromff372fbc30c), as6a06a5421f7: no conflicts, range-diff 60 of 63 commits unchanged and 3 differing only in a context line [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140 changed; pre-commit with all hooks, mypy included, passes against main; on CPU, 269 unit tests pass with transformers 5.18 (the expert-parallel job on two CPU ranks included) and 268 with 5.5.4, three fewer than before because [OMNIML-6054] [1/2] Move fsdp2_wrap out of distributed.py into fsdp2_utils #3140 moved thefsdp2_wraptests to their own file.Rebased on 2026-11-02 onto main
2d4e8d019e7after [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 and [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 merged (from6a06a5421f7), as5829a854612: no conflicts, range-diff 63 of 63 commits unchanged, 492 added source lines; pre-commit with all hooks, mypy included, passes against main; on CPU, 269 unit tests pass with transformers 5.18 (the expert-parallel job on two CPU ranks included) and 268 with 5.5.4.Review fixes, 2026-11-02 (meenchen's reviews of
5829a854612): each calibrate mode descriptor declares_distributed_calibration(True for none, max, mse, smoothquant, awq_clip, gptq, nvfp4_act_headroom and lsq; False for awq_lite, awq_full, svdquant, local_hessian and imatrix) and checks its own options and nested algorithms; a mode that declares neither is refused under a multi-rank plan, and_RANK_LOCAL_CALIBRATIONis gone (c6585dce94d,49911504cf4). The 13 built-in modes refuse what they did before; a test checks every mode modelopt registers declares (it fails with one declaration removed), another that an undeclared mode is refused (it fails when undeclared modes pass).e8d80fed068keeps theget_max_batch_sizeandshard_dataloaderdocstrings to their contract, with the reasons moved to comments. Pre-commit with all hooks passes against main; on CPU, 273 unit tests pass with transformers 5.18 (the expert-parallel job on two CPU ranks included) and 272 with 5.5.4. Claude's review of49911504cf4approved.Before your PR is "Ready for review"
get_max_batch_sizeand the calibration refusals change only for a model whose recorded plan spans several ranks.awq_clip's per-tensor (NVFP4) clip changes under data parallelism, where its replicas now agree (changelog, Bug Fixes).CONTRIBUTING.md: N/AAdditional Information
Part 8 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2739, which has merged.
Summary by CodeRabbit